/*
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package net.javlov.vf;

import java.io.Serializable;
import java.util.*;

import net.javlov.Option;
import net.javlov.State;

/**
 * Stores option-value pairs and keeps track of max options. Only stores options that are eligible for the state to
 * which these values are linked by checking for eligibility at creation time.
 * 
 * Not thread-safe.
 * 
 * @author Matthijs Snel
 *
 */
@SuppressWarnings("rawtypes")
public class TableOptionValues implements Serializable, OptionValues {

	private static final long serialVersionUID = -175768075273183858L;
	
	/**
	 * For postponing potentially expensive update of max options until it is needed
	 */
	private boolean dirty;
	
	protected double maxValue;
	protected Map<Option<?>, Value> optionValues;
	protected List<Option<?>> maxOpts, allOpts;
	
	public TableOptionValues(State s, List<? extends Option> optionPool) {
		this(s, optionPool, null);
	}
	
	public TableOptionValues(State s, List<? extends Option> optionPool, QFunction initializer) {
		optionValues = new HashMap<Option<?>, Value>(optionPool.size());
		dirty = false;
		maxValue = Double.NEGATIVE_INFINITY;

		maxOpts = new ArrayList<Option<?>>( optionValues.size() );
		
		for ( Option o : optionPool )
			if ( o.isEligible(s) )
				addOption(s, o, initializer);
		
		if ( optionValues.isEmpty() )
			throw new IllegalStateException("No eligible options for state " + s);
				
		allOpts = new ArrayList<Option<?>>( optionValues.keySet() );
	}
	
	private void addOption(State s, Option o, QFunction initializer) {
		Value v = new Value( (initializer == null ? 0 : initializer.getValue(s, o)) );
		double val = v.get();
		optionValues.put(o, v);
		updateMax(o, val, val);
	}
	
	/* (non-Javadoc)
	 * @see net.javlov.vf.OptionValues#get(net.javlov.Option)
	 */
	@Override
	public double get(Option o) {
		Value v = optionValues.get(o);
		if ( v == null )
			throw new IneligibleOptionException(o.toString());
		return v.get();		
	}
	
	public void set(Option o, double value) {
		Value val = optionValues.get(o);
		if ( val == null )
			throw new IneligibleOptionException(o.toString());
		double oldVal = val.get();
		val.set(value);
		updateMax(o, oldVal, value);
	}
	
	/* (non-Javadoc)
	 * @see net.javlov.vf.OptionValues#update(net.javlov.Option, double)
	 */
	@Override
	public void update(Option o, double delta) {
		Value val = optionValues.get(o);
		if ( val == null )
			throw new IneligibleOptionException(o.toString());
		double oldVal = val.get();
		val.add(delta);
		updateMax(o, oldVal, val.get());
	}
	
	/* (non-Javadoc)
	 * @see net.javlov.vf.OptionValues#getMaxOptions()
	 */
	@Override
	public List<? extends Option<?>> getMaxOptions() {
		//if ( dirty || !trackMax )
			updateMax();
		return maxOpts;
	}
	
	/* (non-Javadoc)
	 * @see net.javlov.vf.OptionValues#getOptions()
	 */
	@Override
	public List<? extends Option<?>> getOptions() {
		return allOpts;
	}
	
	/* (non-Javadoc)
	 * @see net.javlov.vf.OptionValues#getValues()
	 */
	@Override
	public Collection<Value> getValues() {
		return optionValues.values();
	}
	
	/* (non-Javadoc)
	 * @see net.javlov.vf.OptionValues#size()
	 */
	@Override
	public int size() {
		return optionValues.size();
	}
	
	/* (non-Javadoc)
	 * @see net.javlov.vf.OptionValues#max()
	 */
	@Override
	public double max() {
		//if ( dirty || !trackMax )
			updateMax();
		return maxValue;
	}
	
	/* (non-Javadoc)
	 * @see net.javlov.vf.OptionValues#toString()
	 */
	@Override
	public String toString() {
/*		String s = "{";
		for ( Map.Entry<Option, Value> e : optionValues.entrySet() )
			s += e.getKey() + "=" + e.getValue() + " ";
		s += "}";*/
		return optionValues.toString();
	}
	
	/**
	 * Do a "full" update of the max option (checking all options and their values)
	 */
	protected void updateMax() {
		maxValue = Double.NEGATIVE_INFINITY;
		double val;
		
		for ( Map.Entry<Option<?>, Value> e : optionValues.entrySet() ) {
			val = e.getValue().get();
			if ( val > maxValue ) {
				maxOpts.clear();
				maxOpts.add(e.getKey());
				maxValue = val;
			} else if ( val == maxValue )
				maxOpts.add(e.getKey());
		}
		dirty = false;
	}
	
	/**
	 * Check if max values should be updated based on last updated option
	 * @param o the updated option
	 * @param oldVal old value of the option
	 * @param newVal new value of the option
	 */
	protected void updateMax(Option o, double oldVal, double newVal) {
		//if new option value is larger than current max, update the max
		if ( newVal > maxValue ) {
			maxValue = newVal;
			maxOpts.clear();
			maxOpts.add(o);
			
		//if it equals the max, add option to max options if it was not already there
		} else if ( newVal == maxValue ) {
			if ( !maxOpts.contains(o) ) //assuming will contain very few options anyway
				maxOpts.add(o);
		}
		
		//newVal < maxValue. Remove option from max options
		else if ( oldVal >= maxValue ) {
			maxOpts.remove(o);
			dirty = (maxOpts.size() == 0); //max options are empty; need to do full update
		}
	}
}
